///////////////
// CONSTANTS //
///////////////

const RUN_TESTS = false;
const CALCULATOR_LOADED = true;
const CALCULATOR_NAME = "Calculator";
const CALCULATOR_INPUT = "calc_input";
const UPDATED_URL = "http://code.google.com/p/firefoxcalculator/wiki/Calculator_Updated";
const WINDOW = "window";
const BASE = 10;
const PRECISION = 12;

//////////////////////////
// Class implementation //
//////////////////////////

function Calculator() {
  this.init();
}

Calculator.prototype = {
 result : null,
 history : null,
 functions : null,
 mBranch : null,

 init : function() {
    try {
      // load the preferences
      var prefService = Components.classes["@mozilla.org/preferences-service;1"].getService(Components.interfaces.nsIPrefService);
      this.mBranch = prefService.getBranch("calculator.");

      // MESSY, this test needs fixing!
      // show release notes if the extension has been updated
      // set pref to show release notes if the extension has been used before
      try {
	var val = eval("this.mBranch.getIntPref('" + PREF_NAME_ANGLE + "')");
	this.getPreference(PREF_UPDATED, "Int", 1);
      } catch(e) {  }

      if (this.getPreference(PREF_UPDATED, "Int", 0) == 1 && 0) {
	this.setPreference(PREF_UPDATED, "Int", 0);
	this.openTab(UPDATED_URL);
      }

      // load the main modules
      this.history = new History(this);
      this.functions = new Functions(this);
      var input = document.getElementById(CALCULATOR_INPUT);
      input.focus();

      // Manage UI options
      var aButton = document.getElementById("show-hide-button");
      if (this.getPreference(PREF_NAME_CALC_PAD, "Int") == PAD_YES) {
	document.getElementById("calculator-buttons").hidden = false;
	aButton.image="chrome://calculator/skin/hide.png";
      }
      else {
	document.getElementById("calculator-buttons").hidden = true;
	aButton.image="chrome://calculator/skin/show.png";
      }

      // run tests
      if (RUN_TESTS)
	this.tests();
    } catch (e) {
      alert(e);
    }
  },

 close : function() {
  },
 /////////////////////////
 // Setters and getters //
 /////////////////////////

 setValue : function(id, value) {
    try {
      document.getElementById(id).value = value;
    } catch (e) {
    }
  },


 getValue : function(id) {
    try {
      return document.getElementById(id).value;
    } catch (e) {
    }
    return null;
  },
 getPreference : function(aPref, aType, aDefault) {
    try {
      var val = eval("this.mBranch.get" + aType + "Pref('" + aPref + "')");
      return val;
    } catch(e) {
      this.setPreference(aPref, aType, aDefault);
      return aDefault;
    }
    return null;
  },
 setPreference : function(aPref, aType, aValue) {
    try {
      eval("this.mBranch.set" + aType + "Pref('" + aPref + "'," + aValue + ")");
      return true;
    } catch(e) {
      alert(e);
    }
    return false;
  },


 //////////////
 // Commands //
 //////////////
 enter : function() {
    if (this.getValue(CALCULATOR_INPUT) != "") {
      this.history.addExpression(this.getValue(CALCULATOR_INPUT), this.evaluate(this.getValue(CALCULATOR_INPUT)));
    }
    this.reset();
    //     var input = document.getElementById(CALCULATOR_INPUT);
    //     input.focus();
  },

 reset : function() {
    this.setValue(CALCULATOR_INPUT, "");
    var input = document.getElementById(CALCULATOR_INPUT);
    input.focus();
  },

 exit : function() {
    window.close();
  },
 save : function() {
  },
 help : function() {
    window.open("help.xul", "Help", "chrome");
  },
 about : function() {
    window.openDialog("chrome://calculator/content/about.xul", "About", "chrome");
  },
 showKeys : function() {
    var element = document.getElementById("calculator-buttons");
    var aButton = document.getElementById("show-hide-button");
    this.history.scroll();
    element.hidden = ! element.hidden;
    if (element.hidden) {
      aButton.image="chrome://calculator/skin/show.png";
      this.setPreference(PREF_NAME_CALC_PAD, "Int", PAD_NO);
    } else {
      aButton.image="chrome://calculator/skin/hide.png";
      this.setPreference(PREF_NAME_CALC_PAD, "Int", PAD_YES);
    }
  },
  openTab : function(url) {
    try {
      return Components
	.classes['@mozilla.org/appshell/window-mediator;1']
	.getService(Components.interfaces.nsIWindowMediator)
	.getMostRecentWindow('navigator:browser', true)
	.delayedOpenTab(url, null, null, null, null);
    } catch (e) {
      return window.open(url, "_blank") != null;
    }

  },


 ///////////////////////
 // Interface related //
 ///////////////////////
 addToInput : function(func) {
    var input = document.getElementById(CALCULATOR_INPUT);
    var str = input.value;
    var curspos = input.selectionEnd;
    var res = "";
    var newPos = curspos + func.length;
    if (func.indexOf("(") != -1) {
      func += ")";
    }
    if (this.getPreference(PREF_NAME_FORMAT, "Int", FORMAT_EN) == FORMAT_FR && 0) {
      func = func.replace(new RegExp(',', "gi"), ';');
      func = func.replace(new RegExp('\\.', "gi"), ',');
    }
    res = res.concat(str.slice(0, curspos), func, str.slice(curspos, str.length));
    input.value = res;
    input.setSelectionRange(newPos, newPos);
    input.focus();
  },
 backspace : function() {
    var input = document.getElementById(CALCULATOR_INPUT);
    input.value = input.value.slice(0, input.value.length - 1);
    input.focus();
  },

 checkInput : function() {
    var input = document.getElementById(CALCULATOR_INPUT);
    var str = input.value;
    if (str.length == 1) {
      var c = str[0];
      if ((c == '/' || c == '*' || c == '+' || c == '%' || c == '^') && this.history.index != 0) {
	this.history.current_pos = this.history.index - 1;
	this.history.setInputByKey();
	input.value = input.value.concat(c);
      }
    }
  },

 ///////////////////
 // Core business //
 ///////////////////
 basicEval : function(expr) {
    try {
      var re = new RegExp('__conv__', "gi");
      if (re.test(expr)) {
	return expr;
      }

      var num = new Number(eval(expr));
      num = num.toFixed(PRECISION);
      num = num.toString(BASE);
      if (num == "NaN") {
	num = 0;
      }
      return eval(num);
      // return eval(num+0); // fix for a js bug (deprecated?)
    }
    catch (e) {
      throw WRONG;
    }
  },

 findFunction : function(expr) {
    var i;
    var j;
    for (i = 0; i < expr.length; i++) {
      var c = expr[i];
      // Find functions
      if (!((c >= '0' && c <= '9') ||
	    c == '+' || c == '-' ||
	    c == '*' || c == '/' || c == '.' ||
	    c == '(' || c == ')' || c == '%')) {
	// if it's a base > 10
	for (j = i; j < expr.length; j++) {
	  c = expr[j];
	  if (!((c >= 'a' && c <= 'z') ||
		(c >= '0' && c <= '9'))){
	    break;
	  }
	}
	if (c != '(')
	  continue;

	// Extract the sub expression
	var subexpr = this.getSubExpr(expr, i);

	// Select the function and calculate

	var res = this.functions.selectFunction(subexpr);
	if (res == WRONG_FUNC) {
	  throw WRONG_FUNC;
	}

	// put the calculated value into expr
	var extra = "";
	if (i > 0 && expr[i-1].match(/[0-9]/))
	  extra = "*"
	expr = this.replaceSubstring(expr, subexpr, extra+res);
      }
    }
    return this.basicEval(expr);
  },

 // This is kinda the main thing...
 evaluate : function (expr) {
    try {
      // Cleaning and replacing
      expr = this.formatStylePassIn(expr);
      expr = expr.replace
      (/([.0]*)([0-9]+)/gi, function(s, a, b) {
	return (a.indexOf(".") != -1 ? a.replace(/^0+/, "0") : "") + b;
      });
      expr = this.constantPass(expr);
      expr = expr.toLowerCase();
      expr = this.signPass(expr);
      // The real important line...
      var res = this.formatStylePassOut(this.findFunction(expr));
      var re = /^(-?)([0-9]{4,})([0-9.]*)$/;
      if (res.match(re) && this.getPreference(PREF_NAME_READABILITY, "Int", 0) == 0)
	return res.replace(re, function(str, a, b, c) {
	  return a+(function(x) {
	    var out = "";
	    var i = 0, j = 0;
	    for (i = x.length - 1; i >= 0; i--) {
	      out = x.charAt(i) + out
	      if (j++ % 3 == 2 && i != 0) out = " " + out;
	    }
	    return out;
	  })(b)+c; });
      return res;
    }
    catch (e) {
      return e;
    }
    return WRONG;
  },

 formatStylePassIn : function(expr) {
    expr = expr.replace(new RegExp('%', "gi"), '/100');
    expr = expr.replace(new RegExp('mod', "gi"), '%');
    expr = expr.replace(new RegExp(':.*', "gi"), '');
    expr = expr.replace(new RegExp('//.*', "gi"), '');
    expr = expr.replace(new RegExp(' ', "gi"), '');
    if (this.getPreference(PREF_NAME_FORMAT, "Int", FORMAT_EN) == FORMAT_FR) {
      expr = expr.replace(new RegExp(',', "gi"), '.');
      expr = expr.replace(new RegExp(';', "gi"), ',');
    }
    return expr;
  },

 formatStylePassOut : function(expr) {
    var re = new RegExp('__conv__', "gi");
    if (re.test(expr)) {
      expr = expr.replace(new RegExp('__conv__', "gi"), "");
      return expr;
    }

    // fix for a javascrit bug (deprecated?)
    // round

    // expr = Math.round(expr*10000000000)/10000000000;
    //         // if length of precision is 12 cut one !
    //         var str = expr + "";
    //     var re = /\-?(\d+)\.?(\d*)/i;
    //         var found = str.match(re);
    //         if (found != null && found.length == 3 &&
    //         	found[2].length > 11) {
    //           expr = Number(str.substr(0, str.length - 1));
    //         }

    expr += "";
    // decimal sep thing
    if (this.getPreference(PREF_NAME_FORMAT, "Int", FORMAT_EN) == FORMAT_FR) {
      expr = expr.replace(new RegExp(',', "gi"), ';');
      expr = expr.replace(new RegExp('\\.', "gi"), ',');
    }
    return expr;
  },

  constantPass : function(expr) {
    expr = expr.replace(/([0-9])(PI|LN|LOG|SQRT)/gi, function(str, a, b) { return a+"*"+b; });
    expr = expr.replace(new RegExp('LOG10E', "gi"), Math.LOG10E.toString(BASE));
    expr = expr.replace(new RegExp('LOG2E', "gi"), Math.LOG2E.toString(BASE));
    if (!expr.match(/base16to/gi))
      expr = expr.replace(new RegExp('E', "g"), Math.E.toString(BASE));
    expr = expr.replace(new RegExp('LN10', "gi"), Math.LN10.toString(BASE));
    expr = expr.replace(new RegExp('LN2', "gi"), Math.LN2.toString(BASE));
    expr = expr.replace(new RegExp('PI', "gi"), Math.PI.toString(BASE));
    expr = expr.replace(new RegExp('SQRT1/2', "gi"), Math.SQRT1_2.toString(BASE));
    expr = expr.replace(new RegExp('SQRT2', "gi"), Math.SQRT2.toString(BASE));
    return expr;
  },

 // Turns ! into fact() and ^ into pow(,)
 signPass : function(expr) {
    var re = new RegExp('!(-?[0-9.]+)', "gi");
    if (re.test(expr)) {
      expr = expr.replace(re, 'fact($1)');
    }
    re = RegExp("!", "gi");
    if (re.test(expr)) {
      expr = expr.replace(re, 'fact');
    }
    // Handle "^"
    // Find the character ^
    var pos = 0;
    if ((pos = expr.indexOf("^")) == -1) {
      return expr;
    }
    // Get the thing on the right
    var right = "";
    // if the first char is (, find the closing element
    var c = expr[pos + 1];
    if (c == '(' || (c >= 'a' && c <= 'z')) {
      right = this.getSubExpr(expr, pos + 1);

      // else read [\-0-9\.]+
    } else {
      var i = pos + 1;
      for (i; i < expr.length; i++) {
	if (!((i == pos + 1 && expr[i] == '-') ||
	      expr[i] == '.' || ((expr[i] >= '0' && expr[i] <= '9'))))
	  break;
      }
      if (i == pos + 1) {
	throw WRONG;
      }
      right = expr.substring(pos + 1, i);
    }
    //alert(right);

    // Get the thing on the left
    var left = "";
    // if the first char is ), find the opening element
    if (expr[pos - 1] == ')') {
      var i = pos - 1;
      var par = 0;
      for (i; i >= 0; i--) {
	if (expr[i] == ')')
	  par--;
	else if (expr[i] == '(')
	  par++;
	if (par == 0)
	  break;
      }
      if (par != 0)
	throw WRONG;
      for (i; i >= 0; i--) {
	if (!((expr[i] >= 'a' && expr[i] <= 'z') ||
	      (expr[i] >= '0' && expr[i] <= '9')))
	  break;
      }
      left = expr.substring(i, pos);
      // else read [0-9\.]+
    } else {
      var i = pos - 1;
      for (i; i >= 0; i--) {
	if (!(expr[i] == '.' || ((expr[i] >= '0' && expr[i] <= '9'))))
	  break;
      }
      left = expr.substring(i + 1, pos);
    }
    //alert(left);
    // Replace in expr
    expr = this.replaceSubstring(expr, left+"^"+right, "pow("+left+","+right+")");
    //alert(expr);
    return this.signPass(expr);
  },

 ///////////////
 // Utilities //
 ///////////////

 getSubExpr : function(expr, pos) {
    var par = -1;
    var i;
    for (i = pos; i < expr.length; i++) {
      if (expr[i] == '(') {
	if (par == -1) {
	  par = 0;
	}
	par++;
      }
      else if ( expr[i] == ')') {
	par--;
      }
      if (par == 0) {
	break;
      }
    }
    if (par != 0) {
      throw WRONG_PAR;
    }
    return expr.substring(pos, i + 1);
  },

 replaceSubstring : function(inputString, fromString, toString) {
    return inputString.split(fromString).join(toString);
  },

 tests : function() {
    var t = Array(["1+1", "2"],
		  ["Base16To(10,c)", "12"],
		  // ["3262.05+22953.69", "26215.74"],
		  ["4^(2+2)", "256"],
		  ["2+!5+1", "123"],
		  ["12345679*81", "999 999 999"],
		  ["2sin(PI/2)", "2"],
		  ["1002/3+1", "335"],

		  // 16-9-07
		  ["1+log10(5+3)", "1.903089986992"],
		  ["7 mod 3 + 1", "2"],
		  ["700*10%", "70"],
		  ["Base2To(10,010)", "2"],

		  // 11-11-07
		  ["fact(0)", "1"],
		  ["sin(0)", "0"],
		  ["001+.00200100+0.0012+00.0012+1200+0+0091+01",
		   "1 293.004401"]
		  );
    // var t = Array(["sin(0)", "0"]);
    var n = 0;
    var errors = "";
    for (var i in t) {
      var res = this.evaluate(t[i][0]);
      if (res != t[i][1]) {
	errors += t[i][0]+" = "+res+"\n";
	n += 1;
      }
    }
    // needs firebug!
   try{
     console.debug(t.length+" tests,  "+n+" error(s)\n"+errors);
   } catch (e){
     ;
   }
  }
}
